describe("Scope", function () {
    it("can be constructed and used as an object", function () {
        const scope = new Scope();
        scope.aProperty = 1;

        expect(scope.aProperty).toBe(1);
    });

    describe("digest", function () {
        let scope;
        // 每个测试it执行前都对 scope 初始化
        beforeEach (() => { scope = new Scope(); })

        it("calls the listener function of a watch on first $digest", function () {
            const watchFn = function () { return 'wat'; };
            const listenerFn = jasmine.createSpy();
            scope.$watch(watchFn, listenerFn);

            // 期望执行 $digest 方法时，listenerFn 监听函数可以被调用 
            scope.$digest();

            expect(listenerFn).toHaveBeenCalled();
        })

        it ("calls the watch function with the scope as the arguments", function () {
            const watchFn = jasmine.createSpy();
            const listenerFn = function () {}
            scope.$watch(watchFn, listenerFn);

            scope.$digest();

            expect(watchFn).toHaveBeenCalledWith(scope);
        })

        it ("calls the listener function when the watched value changes", function () {
            scope.someValue = 'a';
            scope.counter = 0;

            // watchFn 为可以接收一个参数 scope 的函数
            const watchFn = function (scope) {
                return scope.someValue;
            }
            // listenerFn 为接收 newValue, oldValue, scope 三个参数的函数
            const listenerFn = function (newValue, oldValue, scope) {
                scope.counter++;        // 每次调用让 counter++
            }
            scope.$watch(watchFn, listenerFn);

            expect(scope.counter).toBe(0);

            scope.$digest();
            expect(scope.counter).toBe(1);

            scope.$digest();
            expect(scope.counter).toBe(1);

            scope.someValue = 'b';
            expect(scope.counter).toBe(1);

            scope.$digest();
            expect(scope.counter).toBe(2);
        })

        it("calls listener when watch value is first undefined", function () {
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.someValue; }, // 此时 someValue 为undefined
                function (newValue, oldValue, scope) { scope.counter++; }
            )

            scope.$digest();
            expect(scope.counter).toBe(1);
        })

        // 第一个调用 lisenterFn 时，将 oldValue 替换为 newValue，防止私有函数 initWatchVal 的引用泄露
        it("calls listener with new value as old value the first time", function () {
            scope.someValue = 123;
            let oldValueGiven;

            scope.$watch(
                function (scope) { return scope.someValue; },
                function (newValue, oldValue, scope) { oldValueGiven = oldValue; }      
            )

            scope.$digest();
            expect(oldValueGiven).toBe(123);
        })

        it("may have watchers that omit the listener function", function () {
            let watchFn = jasmine.createSpy().and.returnValue('something');
            scope.$watch(watchFn);

            scope.$digest();

            expect(watchFn).toHaveBeenCalled();
        })

        it("triggers chained watchers in the same digest", function () {
            scope.name = 'Mango';

            scope.$watch(
                function (scope) { return scope.nameUpper; }, // 此时 nameUpper 为 undefined
                function (newValue, oldValue, scope) {
                    if (newValue) {
                        scope.initial = newValue.substring(0, 1) + '.'; // 此时给 initial 赋值
                    }
                }
            )

            scope.$watch(
                function (scope) { return scope.name; },
                function (newValue, oldValue, scope) {
                    if (newValue) {
                        scope.nameUpper = newValue.toUpperCase();       // 此时给 nameUpper 赋值
                    }
                }
            )

            scope.$digest();
            expect(scope.initial).toBe('M.');

            scope.name = "Bob";
            scope.$digest();
            expect(scope.initial).toBe('B.');
        })

        it("gives up on the watches after 10 iterations", function () {
            scope.counterA = 0;
            scope.counterB = 0;

            // 循环依赖
            scope.$watch(
                function (scope) { return scope.counterA; },
                function (newValue, oldValue, scope) { scope.counterB++; }
            )

            scope.$watch(
                function (scope) { return scope.counterB; },
                function (newValue, oldValue, scope) { scope.counterA++; }
            )

            expect(function() {scope.$digest()}).toThrow();
        })

        it("ends the digest when the last watch is clean", function () {
            let watchExecutions = 0;
            scope.array = []
            for(let i = 0; i < 100; i++) {
                scope.array[i] = i;
                scope.$watch(
                    function (scope) { 
                        watchExecutions++;
                        return scope.array[i]; 
                    },
                    function (newValue, oldValue, scope) {}
                )
            }

            scope.$digest();
            expect(watchExecutions).toBe(200);

            scope.array[0] = 420;
            scope.$digest();
            expect(watchExecutions).toBe(301);
        })

        // 如果在 digest cycle 中添加新 watcher,新的 watcher 也应该在该循环中执行
        it("does not end digest so that new watches are not run", function () {
            scope.aValue = 'abc';
            scope.counter = 0;

            scope.$watch(
                function (scope) {  return scope.aValue; },
                function (newValue, oldValue, scope) {
                    scope.$watch(
                        function (scope) { return scope.aValue; },
                        function (newValue, oldValue, scope) { scope.counter++; }
                    );
                }
            )

            scope.$digest();
            expect(scope.counter).toBe(1);
        })

        it("compares based on value if enabled", function () {
            scope.value = [1, 2, 3];
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.value; },
                function (newValue, oldValue, scope) {
                    scope.counter++;
                },
                true    // true 表示按值比较， false 为按引用比较
            )
            scope.$digest();
            expect(scope.counter).toBe(1);

            scope.value.push(4);
            scope.$digest();
            expect(scope.counter).toBe(2);
        })

        it("correctly handles NaNs", function () {
            scope.number = 0/0;     // NaN
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.number; },
                function (newValue, oldValue, scope) {
                    scope.counter++;
                }
            );

            scope.$digest();
            expect(scope.counter).toBe(1);

            scope.$digest();
            expect(scope.counter).toBe(1);
        })

        it("executes $eval'ed function and returns result", function () {
            scope.aValue = 42;

            const result = scope.$eval(function (scope) {
                return scope.aValue;
            })

            expect(result).toBe(42);
        })

        it("passes the second $val argument straight through", function () {
            scope.aValue = 42;

            const result = scope.$eval(function(scope, arg) {
                return scope.aValue + arg;
            }, 2);

            expect(result).toBe(44);
        })

        it("executes $apply'ed function and starts the digest", function () {
            scope.aValue = 'someValue';
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scope) { scope. counter++; }
            )

            scope.$digest();
            expect(scope.counter).toBe(1);

            scope.$apply(function (scope) {
                scope.aValue = 'someOtherValue';
            });
            expect(scope.counter).toBe(2);
        })

        it("executes $evalAsync'ed function later in the same cycle", function () {
            scope.aValue = [1, 2, 3];
            scope.asyncEvaluated = false;
            scope.asyncEvaluatedImmediately = false;

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scope) {
                    scope.$evalAsync(function (scope) {
                        scope.asyncEvaluated = true;
                    })
                    scope.asyncEvaluatedImmediately = scope.asyncEvaluated;
                }
            )

            scope.$digest();
            expect(scope.asyncEvaluated).toBe(true);
            expect(scope.asyncEvaluatedImmediately).toBe(false);
        })

        it("executes $evalAsync'ed functions added by watch functions", function () {
            scope.value = [1, 2, 3];
            scope.asyncEvaluated = false;

            scope.$watch(
                function (scope) {
                    if (!scope.asyncEvaluated) {
                        scope.$evalAsync(function (scope) {
                            scope.asyncEvaluated = true;
                        });
                    }
                    return scope.value;
                },
                function (newValue, oldValue, scope) {}
            );

            scope.$digest();

            expect(scope.asyncEvaluated).toBe(true);
        })

        it("executes $evalAsync'ed functions even when not dirty", function () {
            scope.value = [1, 2, 3];
            scope.asyncEvaluatedTimes = 0;

            scope.$watch(
                function (scope) {
                    if (scope.asyncEvaluatedTimes < 2) {
                        scope.$evalAsync(function (scope) {
                            scope.asyncEvaluatedTimes++;
                        });
                    }
                    return scope.value;
                },
                function (newValue, oldValue, scope) {}
            );

            scope.$digest();

            expect(scope.asyncEvaluatedTimes).toBe(2);
        })

        it ("eventually halts $evalAsyncs added by watches", function () {
            scope.aValue = [1, 2, 3];

            scope.$watch(
                function (scope) {
                    scope.$evalAsync(function (scope) {});
                    return scope.aValue;
                },
                function (newValue, oldValue, scope) {}
            );

            expect(function() { scope.$digest(); }).toThrow();
        })

        it("has a $$phase field whose value is the current digest phase", function () {
            scope.aValue = [1, 2, 3];
            scope.phaseInWatchFunction = undefined;
            scope.phaseInListenerFunction = undefined;
            scope.phaseInApplyFunction = undefined;

            scope.$watch(
                function (scope) {
                    scope.phaseInWatchFunction = scope.$$phase;
                    return scope.aValue;
                },
                function (newValue, oldValue, scope) {
                    scope.phaseInListenerFunction = scope.$$phase;
                }
            );

            scope.$apply(function (scope) {
                scope.phaseInApplyFunction = scope.$$phase;
            })

            expect(scope.phaseInWatchFunction).toBe('$digest');
            expect(scope.phaseInListenerFunction).toBe('$digest');
            expect(scope.phaseInApplyFunction).toBe('$apply');
        })

        it("schedules a digest in $evalAsync", function (done) {
            scope.aValue = 'abc';
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scope) {
                    scope.counter++;
                }
            );

            scope.$evalAsync(function (scope) {});

            expect(scope.counter).toBe(0);

            setTimeout(function() {
                expect(scope.counter).toBe(1);
                done();
            }, 50);
        })

        it("allows async $apply with $applyAsync", function (done) {
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.aValue;},
                function (newValue, oldValue, scope) { scope.counter++; }
            );

            scope.$digest();
            expect(scope.counter).toBe(1);

            scope.$applyAsync(function (scope) {
                scope.aValue = 'abc';
            })
            expect(scope.counter).toBe(1);

            setTimeout(() => {
                expect(scope.counter).toBe(2);
                done();
            }, 50)
        })

        it("never executes $applyAsync'ed function in the same cycle", function (done) {
            scope.aValue = [1, 2, 3];
            scope.asyncApplied = false;

            scope.$watch(
                function (scope) {return scope.aValue;},
                function (newValue, oldValue, scope) {
                    scope.$applyAsync(function (scope) {
                        scope.asyncApplied = true;
                    })
                }
            );

            scope.$digest();
            expect(scope.asyncApplied).toBe(false);
            setTimeout(() => {
                expect(scope.asyncApplied).toBe(true);
                done();
            }, 50);
        })

        it("cancels and flushes $applyAsync if digested first", function (done) {
            scope.counter = 0;

            scope.$watch(
                function (scope) {
                    scope.counter++;
                    return scope.aValue;
                },
                function (newValue, oldValue, scope) {}
            );

            scope.$applyAsync(function (scope) {
                scope.aValue = 'abc';
            })
            scope.$applyAsync(function (scope) {
                scope.aValue = 'def';
            })

            scope.$digest();
            expect(scope.counter).toBe(2);
            expect(scope.aValue).toEqual('def');

            setTimeout(() => {
                expect(scope.counter).toBe(2);
                done();
            }, 50);
        })

        it("runs a $$postDigest function after each digest", function () {
            scope.counter = 0;
            
            scope.$$postDigest(function () {
                scope.counter++;
            })

            expect(scope.counter).toBe(0);

            scope.$digest();
            expect(scope.counter).toBe(1);

            scope.$digest();
            expect(scope.counter).toBe(1);
        })

        it("does not include $$postDigest in the digest", function () {
            scope.aValue = 'original value';

            scope.$$postDigest(function () {
                scope.aValue = 'changed value';
            })

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scope) { scope.watchedValue = newValue; }
            );

            scope.$digest();
            expect(scope.watchedValue).toBe('original value');

            scope.$digest();
            expect(scope.watchedValue).toBe('changed value');
        })

        it("catches exceptions in watch functions and continues", function () {
            scope.aValue = 'abc';
            scope.counter = 0;

            scope.$watch(
                function (scope) { throw 'error'; },
                function (newVlaue, oldValue, scope) {}
            );

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scopr) { scope.counter++; }
            );

            scope.$digest();
            expect(scope.counter).toBe(1);
        })

        it("catches exceptions in listener functions and continues", function () {
            scope.aValue = 'abc';
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scope) { throw 'Error'; }
            );

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scope) { scope.counter++; }
            );

            scope.$digest();
            expect(scope.counter).toBe(1);
        })

        it("catches exceptions in $evalAsync", function (done) {
            scope.aValue = 'abc';
            scope.counter = 0;

            scope.$watch(
                function (scope) { return scope.aValue; },
                function (newValue, oldValue, scope) {
                    scope.counter++;
                }
            );

            scope.$evalAsync(function (scope) {
                throw 'Error $evalAsync';
            });

            setTimeout(() => {
                expect(scope.counter).toBe(1);
                done();
            }, 50);

        })

        it("catches exceptions in $applyAsync", function (done) {
            scope.$applyAsync(function (scope) {
                throw 'Error $applyAsync1';
            });
            scope.$applyAsync(function (scope) {
                throw 'Error $applyAsync2';
            });
            scope.$applyAsync(function (scope) {
                scope.applied = true;
            });

            setTimeout(() => {
                expect(scope.applied).toBe(true);
                done();
            }, 50);
        })

        it("catches exceptions in $$postDigest", function () {
            let didRun = false;

            scope.$$postDigest(function () {
                throw 'Error $$postDigest';
            });

            scope.$$postDigest(function () {
                didRun = true;
            });

            scope.$digest();
            expect(didRun).toBe(true);
        })
    })
})